查看原文
其他

Jetpack Compose自定义Layout详解

蔡志学 搜狐技术产品 2022-12-21


  

本文字数:10105

预计阅读时间:26分钟


前言

最近在使用Jetpack Compose进行应用开发,不得不说相比较命令式布局方式Xml,Compose声明式UI真香。然而仅使用基础的Compose控件在面对UI大佬的设计图时,总会出现一些“小问题”,包括部分页面不能通过标准布局实现、需要特别精准的控制测量和放置的场景、需要实现布局动画等,这个时候往往需要通过自定义布局去解决这些“小问题”,来达到项目验收标准。既然是要自定义组件,那么首先了解一下Compose的布局模型吧,从布局模型中找到自定义组件的切入点。

布局模型

Compose布局模型可以分为三个部分 组合、布局、绘制

首先组合阶段会执行可组合函数,从而创建界面树,举个例子

上图就是按照 SearchResult 组合函数内部子组件顺序生成界面树的示例。日常开发中可组合函数内部可能会包含各种状态逻辑和控制流,反映到组合阶段就是根据不同的逻辑控制来生成不同的界面树。

接下来的 Layout(布局)  阶段会遍历界面树,通过测量组件以确定自身大小以及在手机上应该摆放的位置,这点和View视图的 onMeasure 和 onLayout 类似,都是先测量然后放置,但是也有不一样的地方,View视图可以经过多次测量来确定子视图的大小和位置信息,但是在Compose中每个组件只允许测量一次,多次测量会报错,正是这一特性可以让我们在使用过程中任意嵌套组件而不会造成很大的性能问题。

最后的 Draw(绘制) 阶段会再次遍历界面树,这个阶段主要做的事情就是把组件绘制到界面上。主要涉及 Canvas 操作,Compose 中画布操作和Android中画布操作大同小异,函数式风格的API使用起来会更方便,虽然有些画布操作 Compose 没有支持,但我们也可以通过 drawIntoCanvas 方法拿到原生Android 环境下的 Canvas 和 Paint 对象进行绘制。这个阶段本文不做深入探讨。

在了解了Compose的布局三个阶段之后,要想自定义组件,切入点还是得从 Layout(布局) 阶段开始,本文主要对该阶段进行深入探讨。

Layout阶段主要涉及了三个步骤:

  • 测量所有子项
  • 确定自己的尺寸
  • 放置其子项

这个看起来跟Android中onMeasure和onLayout的合并,其实在Compose中任何组件都是基于Layout(...)可组合函数进行搭建的,接下来通过源码剖析来探究其内部做了什么? 如果要自定义需要如何做?

Layout可组合函数

@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
)
 {
   ...
}

Layout()可组合函数构成很简单

  • content里面可以放置子组件布局
  • modifier修饰符设置大小、背景等修饰都靠它,其实还可以做更多事后面专门分析。
  • measurePolicy负责测量和放置逻辑。

MeasurePolucy是一个接口,需要组件自己实现,像我们经常使用的ColumnRow等组件都是通过实现自己的逻辑来达到不同的约束效果。我们如果要自己实现一个Column怎么做呢? 首先需要声明我们自己的可组合函数,内部调用Layout()然后在measurePolicy中实现测量和放置逻辑,代码如下:

@Composable
fun CustomColumnLayout(
   modifier: Modifier, 
   content: @Composable () -> Unit  
)
{
    Layout(
        content = content,
        modifier = modifier,
        //这个地方为了更加直观使用了 object 匿名函数声明方式,当然也可以简化成lambda表达式
        //measurePolicy = MeasurePolicy { measurables, constraints -> 
        //      
        //}) 
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            )
: MeasureResult {
              //测量和放置逻辑
              ...
            }
        })
}

constraints表示父元素允许子元素的尺寸范围,如果子元素从该范围内选择了尺寸,那么父元素必须接受并处理子元素。尺寸范围包含最大最小高度和最大最小宽度,当最大最小高度/宽度相等的时候表示高度/宽度为确定值。

class Constraints {
    val minWidth: Int
    val maxWidth: Int
    val minHeight: Int
    val maxHeight: Int
}

measurables表示子元素布局列表,  Measurable可以通过measure(constraints: Constraints)方法进行测量并得到一个新的可放置的布局Placeable。所有子元素测量完成后,在根据得到的测量结果进行具体的放置工作,用代码体现出来如下:

@Composable
fun CustomColumnLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
)
 {
    Layout(
         modifier = modifier,
         content = content
    ) { measurables: List<Measurable>,
         constraints: Constraints ->
        // placeables 是经过测量的子元素,它拥有自身的尺寸值
        val placeables = measurables.map { measurable ->
            // 测量所有子元素,这里不编写任何自定义测量逻辑,只是简单地
            // 调用 Measurable 的 measure 函数并传入 constraints
            measurable.measure(constraints)
        }
        val width = placeables.sumOf { it.height }// 根据 placeables 计算得出
        val height = placeables.maxOf { it.width }// 根据 placeables 计算得出
        // 报告所需的尺寸
        layout (width, height) {
            var y = 0
            placeables.foreach { placeable ->
                //通过遍历将每个项目放置到最终的预期位置
                 placeable.placeRelative(x = 0, y = y)
                // 按照所放置项目的高度增加 y 坐标值
                 y += placeable.height            
            }
        }
   }
}

从代码中可以看到,测量得到的placebles提供了placeRelative()方法,我们可以指定xy值来确定位置,此方法还有额外的作用,如果布局方向为从右到左,则给定的位置将被水平镜像。相对应的如果不想对布局从右到左还是从左到右作出反应可以使用place()方法,该方法只通过指定xy值来确定位置。到这里一个属于自己的Column组件就搞定了,使用起来也很简单

@Composable
fun Test(){
    CustomColumnLayout{
        Text(text = "test1")
        Text(text = "text2")
        Text(text = "text3")
    }
}

接下来我们将分析Layout()可组合函数的另一个参数Modifier,在Compose开发中Modifier起着非常重要的作用,我们使用标准布局也好,使用自定义布局也好都离不开Modifier,那么它究竟是如何运作的呢?我们可不可以通过Modifier来对已有的组件进行自定义布局信息呢?带着这两个疑问让我们进入Modifier的世界去寻找答案。

修饰符

Compose中修饰符虽然有很多种,但它们的基类Modifier其实没有多复杂

interface Modifier {
    fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
    fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
    fun any(predicate: (Element) -> Boolean)Boolean
    fun all(predicate: (Element) -> Boolean)Boolean

    infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)

    interface Element : Modifier {
        override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
            operation(this, initial)

        override fun any(predicate: (Element) -> Boolean)Boolean = predicate(this)

        override fun all(predicate: (Element) -> Boolean)Boolean = predicate(this)
    }

    companion object : Modifier {
        override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
        override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
        override fun any(predicate: (Element) -> Boolean)Boolean = false
        override fun all(predicate: (Element) -> Boolean)Boolean = true
        override infix fun then(other: Modifier): Modifier = other
        override fun toString() = "Modifier"
    }
}

如果我们把没有默认实现的foldIn()foldOut()any()all()方法屏蔽掉

interface Modifier {
             ...
    infix fun then(other: Modifier): Modifier =
       if (other === Modifier) this else CombinedModifier(this, other)

    interface Element : Modifier {
             ...
     }

    companion object : Modifier {
             ...
    override infix fun then(other: Modifier): Modifier = other
    override fun toString() = "Modifier"
  }
}

可以很明显的看到Element以及Modifier的伴生对象都是实现了Modifier接口,其实还有一个类也实现了,就是CombineModifier这个类,它在"合并"不同的Modifier以及遍历的时候很关键。总结一下直接实现Modifier接口的三个类:

  • Element接口
  • Modifier接口伴生对象
  • CombineModifier

我们通过一段代码来更加深入的了解Modifier的运作原理

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(5.dp)
        .background(Color.White)
) {
    ...
}

上面代码通过Modifier设置了大小、边距和背景色。代码是链式调用,通过深入源码看下究竟是如何串起来的,从调用链第一个fillMaxSize()着手分析

fun Modifier.fillMaxSize(fraction: Float = 1f) =
    this.then(if (fraction == 1f) FillWholeMaxSize else createFillSizeModifier(fraction))

fillMaxSize()是以扩展函数的方式声明并返回Modifier实例,this.then(...)因为是链式调用第一个所以调用的应该是Modifier接口伴生对象内部默认实现的then(...)方法

interface Modifier {
             ...
   companion object : Modifier {
             ...
      override infix fun then(other: Modifier): Modifier = other
             ...
  }
}

then(...)方法非常简单直接返回传入的other,在这里应该是FillModifier,所以当前的Modifier就是FillModifier

接下来是padding(5.dp)

fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(
           ...
        )
   )

这里this.then(...)调用的是FillModifier中实现的方法,FillModifier内部没有重写该方法我们向上查找,根据继承链 FillModifier->LayoutModifier->Element->Modifier,最终找到then()方法的实现是在顶层Modifier接口中

interface Modifier {
    infix fun then(other: Modifier): Modifier =
       if (other === Modifier) this else CombinedModifier(this, other)
}

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier

if(other === Modifier)这个条件反编译成java后查看是等同于if(other == Modifier.Companion),因为伴生对象也是实现Modifier接口的。所以这里可以直接把传入的other实例和接口的伴生对象对比,如果相等直接返回,如果不等则使用CombinedModifier进行处理。CombinedModifier的处理也比较简单,接收两个Modifier类型参数分别定义为outerinner,经过链式调用的第二步我们的当前的Modifier结构如下:

链式调用最后一个background(Color.White)

fun Modifier.background(
    color: Color,
    shape: Shape = RectangleShape
)
 = this.then(
    Background(
        ...
    )
)

这里跟上面一样都是通过调用then()方法将Background与之前的Modifier合并,这里Background是实现DrawModifier接口的,那么经过这一轮调用我们可以得到类似上图的结构,只不过多了一层,以此类推如果我们通过Modifier创建了很长的调用链都可以转换为下图:

最后生成的Modifier链会被传入到Layout(...)可组合函数中,在函数中通过materializerOf(modifier)方法进行处理

@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
)
 {
    ...
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            ...
        },
        skippableUpdate = materializerOf(modifier), //这里处理modifier
        content = content
    )
}

materializerOf方法中使用到了两个方法foldIn()foldOut(),前面提到我们在组合函数中声明的Modifier修饰都会转成对应的Modifier链,foldIn()foldOut()就是Modifier提供的遍历方法。

//Element中实现
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = operation(initial, this)
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = operation(this, initial)

//CombinedModifier中实现
 override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
     inner.foldIn(outer.foldIn(initial, operation), operation)

 override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
     outer.foldOut(inner.foldOut(initial, operation), operation)

从代码中我们可以看出foldIn()foldOut()正好相反,拿之前示例的代码进行测试,比如打印出Modifier链的长度

val modifier = Modifier
    .fillMaxSize()
    .padding(5.dp)
    .background(Color.White)
val result = modifier.foldIn(0) { index, element ->
    Log.d("Modifier""index = $index element = $element")
    index + 1
}

使用foldIn()输出结果

image-20220615151315538

使用foldOut()输出结果

image-20220615151552354

对比结果,我们发现CombinedModifier并没有输出,看来CombinedModier`仅仅是作为内部数据结构在使用,官方如此设计应该是希望对上层开发者无感知。

回到materializerOf中继续分析,最终调用Composer的materialize方法,在这个方法内判断如果Modifier链中没有ComposedModifier则不做处理并返回原Modifier链,否则通过foldIn()进行遍历,遇到ComposeModifer类型直接通过它提供的工厂方法构造新的Modifier,生成新的Modifier也有可能还是ComposerModifer继续重复执行,最终得到Modifier链是不包含ComposerModifier的,即是完全摊开的Modifier链。

fun Composer.materialize(modifier: Modifier): Modifier {
    if (modifier.all { it !is ComposedModifier }) return modifier
    ...
    val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
        acc.then(
            if (element is ComposedModifier) {
                ...
                val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                val composedMod = factory(Modifier, this0)
                materialize(composedMod)
            } else element
        )
    }
    ...
    return result
}

其实这仅仅是开始,Modifier链的核心处理是通过LayoutNode来实现的,在LayoutNode中进一步生成LayoutNodeWrapper链,节点转换有很多种,我们拿LayoutModifier来分析,在节点转换时候会包装成ModifiedLayoutNode,然后在Layout()可组合函数中通过Measurable调用measure方法时会触发ModifiedLayoutNode中的measure方法,在内部会触发LayoutModifiermeasure()方法,该方法的实现都是在LayoutModifier的子类中,我们拿PaddingModifier来具体分析:

private class PaddingModifier(
     ...
  ) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
     ...
    override fun MeasureScope.measure(
        measurable: Measurable,//下一个 LayoutNodeWrapper 节点
        constraints: Constraints//来自父容器或者来自上一个节点的约束
    )
: MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        //测量下一个节点
        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }
}

一个元素的所有LayoutNodeWrapper节点测量完成后,会通过Layout()可组合函数中的MeasurePolicy实现继续测量子元素,重复流程直到所有的子元素测量完成。

分析到这里我们就可以发现自定义Layout除了通过自定义实现measurePolicy之外,通过自定义Modifier也可以达到修改布局的目的。这两者就类似于自定义ViewGroup和自定义View,通过Layout()可组合函数自定义相当于自定义ViewGroup,通过Modifier自定义相当于自定义View。对于第二种自定义方式可以通过Modifier提供的layout方法去实现

fun Modifier.customLayout() = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints) //测量
        //在这里可以增加逻辑代码 如果需要的话   
        ...                              
    layout(constraints.maxWidth, constraints.maxHeight) {
        //位置放置 可以实现自己的放置逻辑
        placeable.placeRelative(00)
    }
}

另外官方文档实现的firstBaselineToTop就是一个很不错的例子值得一看。

总结

通过Compose布局模型,着重分析了组合->布局->绘制三个阶段中的布局阶段,以实例和源码结合作为切入点总结出了 Compose中自定义Layout的两种方式

  • 通过调用Layout可组合函数重写measurePolicy,内部实现自己的测量和放置逻辑,跟Android中自定义ViewGroup类似。
  • 实现Modifierlayout方法,内部实现自己的测量和放置逻辑,跟Android中自定义View类似。

这两种布局方式可以根据具体需求灵活应用。另外Compose中组件都是可组合函数,自定义属性可以通过可组合函数参数传递,代码更加直观,使用起来更方便。

以上就是本文的全部内容啦,谢谢阅读。

引用

官方文档:自定义布局  | Jetpack Compose  | Android Developers https://developer.android.com/jetpack/compose/layouts/custom?hl=zh-cn





也许你还想看

(▼点击文章标题或封面查看)

Kotlin攻略(深度解读版)

2021-04-22

源码分析-Kotlin中协程的挂起和恢复

2022-01-06

try catch finally机制分析

2022-08-04

子线程更新UI全解

2022-04-07

Flutter状态管理-Provider的使用和源码解析

2022-02-10


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存